/*
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
 *
 * The Initial Developer of the Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
 * Portions created by Mark A. Kobold are Copyright (C) 2000-2007. All Rights Reserved.
 *
 * Contributor(s): 
 *  Mark A. Kobold [mkobold <at> isqlviewer <dot> com].
 *  
 * If you didn't download this code from the following link, you should check
 * if you aren't using an obsolete version: http://www.isqlviewer.com
 */
package org.isqlviewer.swing;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragGestureRecognizer;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.Date;
import java.util.Hashtable;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIDefaults;
import javax.swing.event.MouseInputAdapter;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableModelEvent;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;

import org.isqlviewer.swing.table.EnhancedTableModel;
import org.isqlviewer.swing.table.Sortable;
import org.isqlviewer.ui.laf.EnhancedTableCellRenderer;
import org.isqlviewer.ui.laf.SortableHeaderRenderer;

/**
 * TODO Add EnhancedTable Object overview JavaDoc information.
 * <p>
 * 
 * @author Mark A. Kobold
 * @version 1.0
 */
public class EnhancedTable extends JTable {

    private static final long serialVersionUID = -9002910430801390897L;
    public static final int INPUT_EVENT_DOUBLE_CLICK = 0x01FF;
    public static final int INPUT_EVENT_COMMAND_CLICK = 0x02FF;
    public static final int INPUT_EVENT_HEADER_CLICK = 0x03FF;
    public static final String CLIENT_PRINTING = "isPrinting";
    public static final String CLIENT_JPOPUP_MENU = "EnhancedTable.PopupMenu";
    private static DragSource dndDragSource = DragSource.getDefaultDragSource();
    protected SortableHeaderRenderer header = new SortableHeaderRenderer(this);
    protected MouseInputAdapter mouseInputHandler = null;
    protected int defaultSortedColumn = -1;
    protected boolean selectionToggle = true;
    protected DnDSupport dndHandler = new DnDSupport(this);
    protected DragGestureRecognizer dndRecognizer = null;

    public EnhancedTable(TableModel model) {

        this();
        setModel(model);
    }

    public EnhancedTable() {

        super(0, 0);
        super.setDragEnabled(false);

        mouseInputHandler = new TableMouseAdapter(this);
        setRowSelectionAllowed(true);
        addMouseListener(mouseInputHandler);
        addMouseMotionListener(mouseInputHandler);
        getTableHeader().setDefaultRenderer(header);
        getTableHeader().addMouseListener(mouseInputHandler);

        setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        InputMap im = getInputMap();
        ActionMap am = getActionMap();
        KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_C, org.isqlviewer.swing.SwingUtilities.MENU_SHORTCUT_MASK);
        im.put(ks, "CELL.COPY");
        am.put("CELL.COPY", new CellCopyAction(this));
        setGridColor(EnhancedTableCellRenderer.gridColor);
    }

    @Override
    protected void createDefaultRenderers() {

        defaultRenderersByColumnClass = new UIDefaults();

        // Numbers
        setLazyRenderer(Number.class, "org.isqlviewer.ui.laf.EnhancedNumberCellRenderer");
        // Doubles and Floats
        setLazyRenderer(Float.class, "org.isqlviewer.ui.laf.EnhancedDecimalCellRenderer");
        setLazyRenderer(Double.class, "org.isqlviewer.ui.laf.EnhancedDecimalCellRendererr");
        // Dates
        setLazyRenderer(Date.class, "org.isqlviewer.ui.laf.EnhancedDateCellRenderer");
        // Booleans
        setLazyRenderer(Boolean.class, "org.isqlviewer.ui.laf.EnhancedBooleanCellRenderer");
        // Everything else
        setLazyRenderer(Object.class, "org.isqlviewer.ui.laf.EnhancedTableCellRenderer");
    }

    public void refreshSorted() {

        Sortable mdl = null;
        if (getModel() instanceof Sortable) {
            mdl = (Sortable) getModel();
        }
        if (mdl == null)
            return;
        int column = header.getSortedColumn();
        if (column == -1)
            return;
        boolean direct = header.isAscending();
        if (mdl.canSort(column, direct)) {
            mdl.sort(column, direct);
        }
    }

    public EnhancedTableModel getSelectedSubModel() {

        int[] rows = null;
        if (selectionToggle) {
            if (getRowSelectionAllowed()) {
                rows = getSelectedRows();
            } else {
                rows = createIterativeArray(getRowCount());
            }
        } else {
            rows = getSelectedRows();
        }

        int[] columns = null;
        if (selectionToggle) {
            if (getColumnSelectionAllowed()) {
                columns = getSelectedColumns();
            } else {
                columns = createIterativeArray(getColumnCount());
            }
        } else {
            columns = getSelectedRows();
        }

        return ((EnhancedTableModel) getModel()).subModel(rows, columns);
    }

    public void setSelectionToggleEnabled(boolean f) {

        selectionToggle = f;
    }

    public void addActionListener(ActionListener al) {

        listenerList.add(ActionListener.class, al);
    }

    public void removeActionListener(ActionListener al) {

        listenerList.remove(ActionListener.class, al);
    }

    public void allowColumnReordering(boolean f) {

        getTableHeader().setReorderingAllowed(f);
    }

    public void allowColumnResizing(boolean f) {

        getTableHeader().setResizingAllowed(f);
    }

    protected void fireActionEvent(ActionEvent e) {

        ActionListener[] lst = listenerList.getListeners(ActionListener.class);
        for (int i = 0; i < lst.length; i++) {
            try {
                lst[i].actionPerformed(e);
            } catch (Throwable t) {
            }
        }
    }

    protected void paintRow(Graphics2D g2, int[] columns, int row, boolean selected) {

        int isw = getIntercellSpacing().width + (4 * SortableHeaderRenderer.SORT_ICON_WIDTH);
        int x = 0;
        for (int i = 0; i < columns.length; i++) {
            TableCellRenderer tcr = getCellRenderer(row, columns[i]);
            Object obj = getValueAt(row, columns[i]);
            Component view = tcr.getTableCellRendererComponent(this, obj, selected, false, row, columns[i]);
            TableColumn column = columnModel.getColumn(columns[i]);
            int headerWidth = column.getWidth() + isw;
            Dimension dim = new Dimension(headerWidth, view.getPreferredSize().height);
            view.setSize(dim);
            view.setLocation(0, 0);
            view.paint(g2);
            x += dim.width;
            g2.translate(dim.width, 0);
            if (x >= g2.getClipBounds().width) {
                break;
            }
        }
    }

    @Override
    protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) {

        int lrow = getSelectedRow();
        boolean f = super.processKeyBinding(ks, e, condition, pressed);
        if (getModel() instanceof EnhancedTableModel) {
            EnhancedTableModel mdl = (EnhancedTableModel) getModel();
            if (mdl == null) {
                return f;
            }
            switch (e.getKeyCode()) {
                case KeyEvent.VK_DOWN :
                case KeyEvent.VK_PAGE_DOWN :
                    if (lrow == getRowCount() - 1) {
                        if (!ks.isOnKeyRelease() && mdl.pageDown()) {
                            setRowSelectionInterval(0, 0);
                            scrollRectToVisible(getCellRect(0, 0, true));
                        }
                    }
                    break;
                case KeyEvent.VK_UP :
                case KeyEvent.VK_PAGE_UP :
                    if (lrow == 0) {
                        if (!ks.isOnKeyRelease() && mdl.pageUp()) {
                            setRowSelectionInterval(getRowCount() - 1, getRowCount() - 1);
                            scrollRectToVisible(getCellRect(0, 0, true));
                        }
                    }
                    break;
            }
        }
        return f;
    }

    @Override
    public void setFont(Font fnt) {

        super.setFont(fnt);
        FontMetrics fm = getFontMetrics(fnt);
        setRowHeight(fm.getHeight());
    }

    @Override
    public void setDragEnabled(boolean f) {

        if (f) {
            if (dndRecognizer == null) {
                dndRecognizer = dndDragSource.createDefaultDragGestureRecognizer(this,
                        DnDConstants.ACTION_COPY_OR_MOVE, dndHandler);
            }
        } else {
            if (dndRecognizer != null) {
                dndRecognizer.resetRecognizer();
                dndRecognizer.removeDragGestureListener(dndHandler);
                dndRecognizer.setComponent(null);
                dndRecognizer = null;
            }
        }
    }

    @Override
    public boolean getDragEnabled() {

        return (dndRecognizer == null);
    }

    @Override
    public synchronized void setModel(TableModel mdl) {

        super.setModel(mdl);
        synchronized (this) {
            if (mdl instanceof Sortable) {
                refreshSorted();
            }
        }
    }

    @Override
    public void tableChanged(TableModelEvent event) {

        super.tableChanged(event);

        if (event.getFirstRow() == TableModelEvent.HEADER_ROW) {
            setColumnSizes(0, getColumnCount());
            try {
                boolean allowed = getTableHeader().getReorderingAllowed();
                allowColumnReordering(allowed && (getColumnCount() >= 2));
            } catch (Throwable t) {
            }
        }
    }

    @Override
    public void columnAdded(TableColumnModelEvent event) {

        if (getModel() != null) {
            setColumnSizes(event.getToIndex(), event.getToIndex());
        }
        super.columnAdded(event);
    }

    /** Sizes the columns with the max size of the header and the columns data. */
    private void setColumnSizes(int from, int to) {

        TableColumnModel cmodel = getColumnModel();
        int columnCount = cmodel.getColumnCount();
        int fromIndex = from;
        int toIndex = to;
        if (fromIndex < 0) {
            fromIndex = 0;
        }
        if (toIndex >= columnCount) {
            toIndex = columnCount - 1;
        }
        if (fromIndex > toIndex)
            return;
        TableModel model = getModel();
        int rowCount = getRowCount();
        TableCellRenderer headerRenderer = getTableHeader().getDefaultRenderer();
        int isw = getIntercellSpacing().width + (4 * SortableHeaderRenderer.SORT_ICON_WIDTH);
        for (int viewCol = fromIndex; viewCol <= toIndex; viewCol++) {
            TableColumn column = cmodel.getColumn(viewCol);
            int headerWidth = getHeaderWidth(headerRenderer, column) + isw;
            int cellWidth = (rowCount == 0) ? 0 : getDataWidth(model, column, viewCol, rowCount);
            column.setPreferredWidth(Math.max(headerWidth, cellWidth));
            if (getColumnClass(viewCol) == Boolean.class && getRowCount() >= 1) {
                TableCellRenderer rend = getCellRenderer(0, viewCol);
                Object o = getValueAt(0, viewCol);
                Component comp = rend.getTableCellRendererComponent(this, o, false, false, 0, viewCol);
                setRowHeight(comp.getPreferredSize().height);
            }
        }
    }

    private void setLazyValue(Hashtable h, Class c, String s) {

        h.put(c, new UIDefaults.ProxyLazyValue(s));
    }

    private void setLazyRenderer(Class c, String s) {

        setLazyValue(defaultRenderersByColumnClass, c, s);
    }

    private int getHeaderWidth(TableCellRenderer headerRenderer, TableColumn column) {

        Component comp = headerRenderer
                .getTableCellRendererComponent(this, column.getHeaderValue(), false, false, 0, 0);
        return comp.getPreferredSize().width;
    }

    private int getDataWidth(TableModel model, TableColumn column, int col, int rowCount) {

        Component comp;
        int modelCol = convertColumnIndexToModel(col);
        TableCellRenderer renderer = column.getCellRenderer();
        if (renderer == null) {
            renderer = getDefaultRenderer(model.getColumnClass(modelCol));
            column.setCellRenderer(renderer);
        }
        if (renderer == null) {
            return 0;
        }

        Boolean b = (Boolean) getClientProperty(EnhancedTable.CLIENT_PRINTING);
        b = (b == null ? Boolean.FALSE : b);
        int maxWidth = 0;
        for (int row = 0; row < rowCount; ++row) {
            Object v = model.getValueAt(row, modelCol);
            comp = renderer.getTableCellRendererComponent(this, v, false, false, row, col);
            if (b.booleanValue()) {
                int current = getRowHeight(row);
                int preferred = comp.getPreferredSize().height;
                preferred = Math.max(preferred, current);
                setRowHeight(row, preferred);
            }
            int thisCellWidth = SortableHeaderRenderer.SORT_ICON_WIDTH;
            thisCellWidth += (comp.getPreferredSize().width + (2 * getIntercellSpacing().width));
            maxWidth = Math.max(maxWidth, thisCellWidth);
        }
        return maxWidth;
    }

    private static int[] createIterativeArray(int size) {

        int[] result = new int[size];
        for (int i = 0; i < size; i++) {
            result[i] = i;
        }
        return result;
    }

    /**
     * Creates a tab delimited text for selected cells of the given JTable.
     * <p>
     * 
     * @param table to copy selection from.
     */
    private static void copySelectedCellsToClipBoard(JTable table) {

        if (table != null) {
            try {
                table.getCellEditor().cancelCellEditing();
            } catch (Throwable t) {

            }
            StringBuffer buff = new StringBuffer("");
            StringBuffer row = new StringBuffer("");
            for (int r = 0; r < table.getRowCount(); r++) {
                for (int c = 0; c < table.getColumnCount(); c++) {
                    if (table.isCellSelected(r, c))
                        row.append(table.getValueAt(r, c) + "\t");
                }

                if (row.toString().trim().length() >= 1) {
                    buff.append(row);
                    buff.append(System.getProperty("line.seperator", "\n"));
                }
                row.setLength(0);
            }

            Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
            StringSelection ss = new StringSelection(buff.toString().trim());
            cb.setContents(ss, ss);
        }
    }

    private static class CellCopyAction extends AbstractAction {

        private static final long serialVersionUID = 8845117056535654319L;
        private EnhancedTable reference = null;

        public CellCopyAction(EnhancedTable table) {

            reference = table;
        }

        public void actionPerformed(ActionEvent e) {

            if (reference != null) {
                if (!reference.getSelectionModel().isSelectionEmpty()) {
                    copySelectedCellsToClipBoard(reference);
                } else {
                    reference.getToolkit().beep();
                }
            } else {
                reference.getToolkit().beep();
            }
        }
    }

    private static class TableMouseAdapter extends MouseInputAdapter {

        private EnhancedTable table = null;

        public TableMouseAdapter(EnhancedTable reference) {

            table = reference;
        }

        @Override
        public void mousePressed(MouseEvent e) {

            Object src = e.getSource();
            if (src instanceof JComponent) {
                JComponent c = (JComponent) src;
                if (handlePopup(e, c)) {
                    return;
                }
            }
            int mods = e.getModifiers();
            int row = table.rowAtPoint(e.getPoint());
            int col = table.columnAtPoint(e.getPoint());
            boolean isShft = (mods & InputEvent.SHIFT_DOWN_MASK) == InputEvent.SHIFT_DOWN_MASK;
            boolean isMeta = (mods & InputEvent.CTRL_DOWN_MASK) == InputEvent.CTRL_DOWN_MASK;
            boolean isAlt = (mods & InputEvent.ALT_MASK) == InputEvent.ALT_MASK;
            boolean isLeft = SwingUtilities.isLeftMouseButton(e);
            boolean isEdit = table.getModel().isCellEditable(row, col);
            int cc = e.getClickCount();
            if (src == table) {
                if (isLeft && cc == 2 && (!isShft && !isMeta)) {
                    if (!isEdit && table.selectionToggle) {
                        table.setRowSelectionAllowed(!(table.getRowSelectionAllowed()));
                        table.repaint();
                        ActionEvent evt = new ActionEvent(table, EnhancedTable.INPUT_EVENT_DOUBLE_CLICK, "");
                        table.fireActionEvent(evt);
                    }
                } else if (cc == 1 && isAlt) {
                    if (row >= 0 && col >= 0) {
                        ActionEvent evt = new ActionEvent(src, EnhancedTable.INPUT_EVENT_COMMAND_CLICK, Integer
                                .toString(col), row);
                        table.fireActionEvent(evt);
                    }
                }
                return;
            } else if (table.getModel() instanceof Sortable) {
                int pcc = 2;
                JTableHeader th = (JTableHeader) src;
                if (th.getResizingColumn() != null) {
                    if (cc == 2) {
                        table.setColumnSizes(0, table.getColumnCount());
                    }
                    // click from resizing columns
                    return;
                }
                Sortable mdl = (Sortable) table.getModel();
                int idx = th.columnAtPoint(e.getPoint());
                if (cc == pcc && isLeft && idx >= 0 && mdl != null) {
                    int sci = table.header.getRenderedSortedColumn();
                    boolean asc = table.header.isAscending();
                    if (!mdl.canSort(idx, asc)) {
                        th.getToolkit().beep();
                        return;
                    }
                    if (sci != idx) {
                        table.header.setSortedColumn(idx, table, false);
                    } else {
                        table.header.setSortedColumn(sci, table, !asc);
                    }
                    asc = table.header.isAscending();
                    idx = table.header.getSortedColumn();
                    synchronized (mdl) {
                        mdl.sort(idx, asc);
                        th.repaint();
                    }
                }
                return;
            }
        }

        @Override
        public void mouseReleased(MouseEvent e) {

            Object src = e.getSource();
            if (src instanceof JComponent) {
                JComponent c = (JComponent) src;
                handlePopup(e, c);
            }
        }

        private boolean handlePopup(MouseEvent e, JComponent jc) {

            JPopupMenu popup = null;
            if (e.isPopupTrigger()) {
                try {
                    popup = (JPopupMenu) jc.getClientProperty(CLIENT_JPOPUP_MENU);
                } catch (Throwable t) {
                    return false;
                }
                if (popup != null) {
                    int row = table.rowAtPoint(e.getPoint());
                    int[] selected = table.getSelectedRows();
                    boolean isSelected = false;
                    for (int i = 0; i < selected.length; i++) {
                        if (selected[i] == row) {
                            isSelected = true;
                            break;
                        }
                    }
                    if (!isSelected) {
                        table.setRowSelectionInterval(row, row);
                    }
                    popup.show(jc, e.getX(), e.getY());
                    e.consume();
                    return true;
                }

                return false;
            }
            return false;
        }
    }

    private static class DnDSupport implements DragGestureListener, DragSourceListener {

        private static final Point pt = new Point(2, 2);
        private static final int MAX_ROWS = 7;
        private EnhancedTable table = null;

        public void dragDropEnd(DragSourceDropEvent dsde) {

        }

        public void dragEnter(DragSourceDragEvent dsde) {

        }

        public void dragExit(DragSourceEvent dse) {

        }

        public void dragOver(DragSourceDragEvent dsde) {

        }

        public void dropActionChanged(DragSourceDragEvent dsde) {

        }

        public DnDSupport(EnhancedTable reference) {

            table = reference;
        }

        public void dragGestureRecognized(DragGestureEvent event) {

            DragGestureRecognizer dndRecognizer = event.getSourceAsDragGestureRecognizer();
            try {
                EnhancedTableModel selection = table.getSelectedSubModel();
                if (selection.isEmpty()) {
                    return;
                }
                int[] rows = null;
                if (table.selectionToggle) {
                    if (table.getRowSelectionAllowed()) {
                        rows = table.getSelectedRows();
                    } else {
                        rows = createIterativeArray(table.getRowCount());
                    }
                } else {
                    rows = table.getSelectedRows();
                }

                int[] columns = null;
                if (table.selectionToggle) {
                    if (table.getColumnSelectionAllowed()) {
                        columns = table.getSelectedColumns();
                    } else {
                        columns = createIterativeArray(table.getColumnCount());
                    }
                } else {
                    columns = table.getSelectedRows();
                }

                Transferable data = selection;
                GraphicsConfiguration gConfig = table.getGraphicsConfiguration();
                Dimension dim = table.getVisibleRect().getSize();
                int height = 0;
                int min = Math.min(MAX_ROWS, rows.length);
                for (int i = 0; i < min; i++) {
                    height += table.getRowHeight(rows[i]);
                }
                int width = 0;
                for (int i = 0; i < columns.length; i++) {
                    width += table.getColumnModel().getColumn(columns[i]).getPreferredWidth();
                }
                dim.setSize(Math.min(dim.width, width), height);
                BufferedImage bi = gConfig.createCompatibleImage(dim.width, height);
                int x = 0;
                int y = 0;
                Graphics2D g2 = bi.createGraphics();
                g2.setColor(Color.WHITE);
                g2.fillRect(0, 0, dim.width, dim.height);
                for (int i = 0; i < rows.length; i++) {
                    int rowHeight = table.getRowHeight(rows[i]);
                    Graphics2D subg = (Graphics2D) g2.create(x, y, dim.width, rowHeight);
                    table.paintRow(subg, columns, rows[i], true);
                    y += rowHeight;
                }
                g2.setColor(new Color(225, 225, 225, 128));
                g2.fillRect(0, 0, dim.width, dim.height);
                g2.setColor(Color.BLACK);
                g2.drawRect(0, 0, dim.width - 1, height - 1);
                dndDragSource.startDrag(event, null, bi, pt, data, this);
            } catch (Throwable t) {
                dndRecognizer.resetRecognizer();
                table.getToolkit().beep();
            }
        }
    }

}